package com.abso.sunlight.explorer;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Status;

import com.abso.sunlight.api.Legislator;
import com.abso.sunlight.api.LegislatorQuery;
import com.abso.sunlight.api.SunlightException;
import com.abso.sunlight.explorer.events.ISearchStateListener;
import com.abso.sunlight.explorer.events.SearchStateChangedEvent;
import com.abso.sunlight.explorer.events.SearchStateChangedEvent.Type;

/**
 * A specialized class able to manage legislator searches.
 */
public class LegislatorSearchManager {

    /** The current index used to name newly created searches. */
    private int searchCount;

    /** The current list of saved searches. */
    private List<LegislatorSearch> savedSearches;

    /** A collection of listeners to be notified of changes in the set of saved searches. */
    private ListenerList searchStateListeners;

    /** Constructs a new search manager. */
    public LegislatorSearchManager() {
        this.searchStateListeners = new ListenerList();
        this.searchCount = 0;
        this.savedSearches = new ArrayList<LegislatorSearch>();
        try {
            deserializeSearches();
        } catch (DocumentException e) {
            CongressExplorerPlugin.getDefault().getLog().log(
                    new Status(IStatus.ERROR, CongressExplorerPlugin.PLUGIN_ID, e.getMessage(), e));
        } catch (SunlightException e) {
            CongressExplorerPlugin.getDefault().getLog().log(
                    new Status(IStatus.ERROR, CongressExplorerPlugin.PLUGIN_ID, e.getMessage(), e));
        }
    }

    /**
     * Returns the next valid search name for a newly created search.
     * 
     * @return the next valid search name.
     */
    public String getNextSearchName() {
        searchCount++;
        while (getSearch("Search " + searchCount) != null) {
            searchCount++;
        }
        return "Search " + searchCount;
    }

    /**
     * Adds a new listener to be notified of changes in the set of saved searches.
     * 
     * @param listener
     *            the listener to add.
     */
    public void addSearchStateListener(ISearchStateListener listener) {
        searchStateListeners.add(listener);
    }

    /**
     * Removes a listener.
     * 
     * @param listener
     *            the listener to remove.
     */
    public void removeSearchStateListener(ISearchStateListener listener) {
        searchStateListeners.remove(listener);
    }

    /**
     * Adds a new search.
     * 
     * @param search
     *            the search to add.
     * @param fromSearch
     *            the search this new source has been derived from (could be null).
     * @throws SunlightException
     *             if an exception occurred adding the search.
     */
    public void addSearch(LegislatorSearch search, LegislatorSearch fromSearch) throws SunlightException {
        search.setSaved(true);
        savedSearches.add(search);
        serializeSearches();
        SearchStateChangedEvent event = new SearchStateChangedEvent(Type.LEGISLATOR_SEARCH_ADDED, search, fromSearch);
        fireSearchStateEvent(event);
    }

    /**
     * Removes an existing saved search.
     * 
     * @param search
     *            the search to delete.
     * @throws SunlightException
     *             if an exception occurred removing the search.
     */
    public void deleteSearch(LegislatorSearch search) throws SunlightException {
        savedSearches.remove(search);
        serializeSearches();
        SearchStateChangedEvent event = new SearchStateChangedEvent(Type.LEGISLATOR_SEARCH_DELETED, search, null);
        fireSearchStateEvent(event);
    }

    /**
     * Renames a saved search.
     * 
     * @param search
     *            the search to rename.
     * @param newName
     *            the new name.
     * @throws SunlightException
     *             if an exception occurred updating the search.
     */
    public void renameSearch(LegislatorSearch search, String newName) throws SunlightException {
        search.setName(newName);
        serializeSearches();
        SearchStateChangedEvent event = new SearchStateChangedEvent(Type.LEGISLATOR_SEARCH_RENAMED, search, null);
        fireSearchStateEvent(event);
    }

    /**
     * Updates a search query.
     * 
     * @param search
     *            the search to be updated.
     * @param newQuery
     *            the new query.
     * @throws SunlightException
     *             if an exception occurred updating the search.
     */
    public void updateSearchQuery(LegislatorSearch search, LegislatorQuery newQuery) throws SunlightException {
        search.setQuery(newQuery);
        serializeSearches();
        SearchStateChangedEvent event = new SearchStateChangedEvent(Type.LEGISLATOR_SEARCH_QUERY_CHANGED, search, null);
        fireSearchStateEvent(event);
    }

    /**
     * Updates the result for a specific search.
     * 
     * @param search
     *            the search to update.
     * @param legislators
     *            the list of matching legislators.
     */
    public void setSearchResult(LegislatorSearch search, List<Legislator> legislators) {
        search.setResult(legislators);
        SearchStateChangedEvent event = new SearchStateChangedEvent(Type.LEGISLATOR_SEARCH_PERFORMED, search, null);
        fireSearchStateEvent(event);
    }

    /**
     * Returns the saved search with a specific name.
     * 
     * @param name
     *            the name of a search.
     * @return the saved search, or <code>null</code> if no search exists with the given name.
     */
    public LegislatorSearch getSearch(String name) {
        name = normalizeSearchName(name);
        for (LegislatorSearch search : savedSearches) {
            if (name.equals(search.getName())) {
                return search;
            }
        }
        return null;
    }

    /**
     * Returns an immutable collection composed by the saved searches.
     * 
     * @return the immutable list of saved searches.
     */
    public List<LegislatorSearch> getSearches() {
        return Collections.unmodifiableList(savedSearches);
    }

    /**
     * Notifies all listeners of a change in the saved search set.
     * 
     * @param event
     *            the event to fire.
     */
    private void fireSearchStateEvent(SearchStateChangedEvent event) {
        Object[] listeners = searchStateListeners.getListeners();
        for (int i = 0; i < listeners.length; ++i) {
            ((ISearchStateListener) listeners[i]).searchStateChanged(event);
        }
    }

    /**
     * Normalizes a search name, removing leading and trailing whitespaces.
     * 
     * @param name
     *            the search name.
     * @return the normalized search name.
     */
    private String normalizeSearchName(String name) {
        return StringUtils.trim(StringUtils.defaultString(name));
    }

    /**
     * Stores into a persistent area the current list of legislator searches.
     * 
     * @throws SunlightException
     *             if an exception occurred storing the searches.
     */
    private synchronized void serializeSearches() throws SunlightException {
        Document searchesDoc = DocumentHelper.createDocument();
        Element searchesElem = searchesDoc.addElement("searches");
        List<LegislatorSearch> searchesToSerialize = new ArrayList<LegislatorSearch>(savedSearches);
        for (LegislatorSearch search : searchesToSerialize) {
            Element searchElem = searchesElem.addElement("legislator_search");
            searchElem.addElement("name").setText(search.getName());
            Document queryDoc = search.getQuery().toXML();
            Element queryElem = queryDoc.getRootElement();
            queryElem.detach();
            searchElem.add(queryElem);
        }
        Writer writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(getSavedSearchesFile()));
            OutputFormat outFormat = OutputFormat.createPrettyPrint();
            outFormat.setEncoding("UTF-8");
            XMLWriter xmlWriter = new XMLWriter(writer, outFormat);
            xmlWriter.setMaximumAllowedCharacter(127);
            xmlWriter.write(searchesDoc);
            xmlWriter.flush();
            xmlWriter.close();
        } catch (IOException e) {
            throw new SunlightException(e);
        } finally {
            IOUtils.closeQuietly(writer);
        }
    }

    /**
     * Restores the saved searches from the predefined state location area.
     * 
     * @throws DocumentException
     *             if an error occurred restoring the searches from their XML persistent storage.
     * @throws SunlightException
     *             if an error occurred restoring the searches fields.
     */
    private synchronized void deserializeSearches() throws DocumentException, SunlightException {
        File savedSearchesFile = getSavedSearchesFile();
        if (savedSearchesFile.isFile() && savedSearchesFile.canRead()) {
            SAXReader reader = new SAXReader();
            Document doc = reader.read(savedSearchesFile);
            Element searchesElem = doc.getRootElement();
            if (searchesElem != null) {
                for (Iterator<?> i = searchesElem.elementIterator("legislator_search"); i.hasNext();) {
                    Element searchElem = (Element) i.next();
                    String searchName = searchElem.valueOf("name");
                    Element queryElem = searchElem.element("legislator_query");
                    if (queryElem != null) {
                        LegislatorQuery query = new LegislatorQuery();
                        query.loadFromXML(queryElem);
                        LegislatorSearch search = new LegislatorSearch();
                        search.setName(searchName);
                        search.setQuery(query);
                        search.setSaved(true);
                        savedSearches.add(search);
                    }
                }
            }
        }
    }

    /**
     * Returns the file system's file designed to store the searches.
     * 
     * @return   the saved searches XML file.
     */
    private File getSavedSearchesFile() {
        return new File(CongressExplorerPlugin.getDefault().getStateLocation().toFile(), "savedSearches.xml");
    }

}
